package ch.ethz.fcl.metrobuzz.render.od.flow_view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

import ch.ethz.fcl.metrobuzz.data.MBData;
import ch.ethz.fcl.metrobuzz.data.od.SelectLinkQuery;
import ch.ethz.fcl.metrobuzz.render.od.flow_view.sankey.ODSankeyDiagramData;
import ch.ethz.fcl.metrobuzz.render.od.flow_view.sankey.SankeyNode;
import ch.ethz.fcl.metrobuzz.render.od.flow_view.sankey.SankeyNodeGeometry;
import ch.ethz.fcl.metrobuzz.render.od.flow_view.sankey.SankeyNodeGroup;
import ch.ethz.fcl.metrobuzz.render.od.flow_view.sankey.ODPair;
import ch.ethz.fcl.metrobuzz.render.techniques.colors.SGMRTColorTable;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Point2D;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Rectangle;
import ch.ethz.fcl.metrobuzz.tools.MBTools;

public class ODComponent extends Component {
	private static final long serialVersionUID = -2676621279347865091L;

	private int width, height;
	private Rectangle bound = null;

	private float originX, destinX; // x coordinates for od pairs

	private SelectLinkQuery slq = null;

	private ODSankeyDiagramData data;

	private int inter_group_gap = 10;
	public static int inner_group_gap = 2;

	private BufferedImage colorMap = null;
	private int imageHeight;
	private int imageWidth;

	private int geoBoundSize = 148;

	public static int uncertainyWidth = 20;

	private static final int TIME_INTERVAL_SIZE = 1;

	private Line2D.Float curTimeLeftLine, curTimeRightLine;

	public ODComponent(SelectLinkQuery slq, ODSankeyDiagramData data,
			Rectangle bound, int width, int height) {
		super.setSize(width, height);

		this.width = width;
		this.height = height;

		this.slq = slq;
		this.bound = bound;

		this.data = data;

		try {
			colorMap = ImageIO.read(new File("res/heatmap/colors2.png"));
			imageHeight = colorMap.getHeight();
			imageWidth = colorMap.getWidth();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void update() {
		if (slq != null)
			data.update();
		calODPositions();
	}

	@Override
	public Dimension getPreferredSize() {
		return new Dimension(width, height);
	}

	@Override
	public void paint(Graphics g) {
		super.paint(g);

		Graphics2D g2d = (Graphics2D) g.create();

		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		g2d.setBackground(Color.WHITE);

		Font font = new Font("Helvetica", Font.PLAIN, 20);
		g2d.setFont(font);

		renderLegend(g2d);

		renderTimeLine(g2d);

		if (slq != null) {
			if (data.getCurOrginSNList() == null
					|| data.getCurOrginSNList().size() == 0
					|| data.getCurDestinSNList() == null)
				return;

			renderODPair(g2d);
			renderODList(g2d);

			// renderCurTimePeriod(g2d);
		}

		g2d.dispose();
	}

	private void renderLegend(Graphics2D g) {
		g.drawString("Origins", bound.getMinX() + 50 + uncertainyWidth,
				bound.getMinY());

		g.drawString("Destinations", bound.getMaxX() - 140 - uncertainyWidth,
				bound.getMinY());
	}

	private void renderColorMap(Graphics2D g, int min, int max) {
		int x = getWidth() / 2;
		int y = 20;
		float scale = 0.4f;
		int height = g.getFontMetrics().getHeight() / 3;

		AffineTransform at = new AffineTransform();

		at.translate(x, y);
		at.scale(scale, scale);
		at.rotate(-Math.PI / 2);

		at.translate(-imageWidth / 2, -imageHeight / 2);

		g.drawImage(colorMap, at, this);

		int minLength = g.getFontMetrics().stringWidth(min + "");

		g.drawString(min + "", (int) (getWidth() / 2 - 0.4 * imageHeight / 2
				- minLength - 2), y + height);
		g.drawString(max + "",
				(int) (getWidth() / 2 + 0.4 * imageHeight / 2 + 2), y + height);
	}

	private void renderTimeLine(Graphics2D g) {
		Font font = new Font("Helvetica", Font.PLAIN, 14);
		g.setFont(font);

		int btmY = (int) bound.getMaxY() - 70;

		originX = bound.getMinX() + 65 + 30 + uncertainyWidth;
		destinX = bound.getMaxX() - 75 - 30 - uncertainyWidth;

		g.setColor(Color.BLACK);
		g.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND,
				BasicStroke.JOIN_ROUND));
		g.drawLine((int) originX, btmY, (int) destinX, btmY);

		for (int i = MBData.startTime; i <= MBData.startTime + MBData.period; i++) {
			float x = originX + (destinX - originX) / (MBData.period)
					* (i - MBData.startTime);
			int height = 10;
			if (i % 4 == 0 || i == MBData.startTime + MBData.period
					|| i == MBData.startTime) {
				g.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND,
						BasicStroke.JOIN_ROUND));

				int m = 0;
				if (MBData.period == 72)
					m = 24;
				else
					m = 4;
				if (i % m == 0 || i == MBData.startTime + MBData.period
						|| i == MBData.startTime) {
					DecimalFormat formater = new DecimalFormat("00");

					String clock = formater.format(i / 4) + ":00";
					if ((i == MBData.startTime + MBData.period || i == MBData.startTime)
							&& i % m != 0)
						clock = formater.format(i / 4) + ":" + (i % m * 15);

					int idLength = g.getFontMetrics().stringWidth(clock);
					g.drawString(clock, x - idLength / 2, btmY + 25);
				}
			} else {
				g.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND,
						BasicStroke.JOIN_ROUND));
				height = 5;
			}

			g.drawLine((int) x, btmY + 1, (int) x, btmY + height);
		}
	}

	void renderCurTimePeriod(Graphics2D g) {
		g.setColor(Color.BLACK);

		originX = bound.getMinX() + 65 + 30 + uncertainyWidth;
		destinX = bound.getMaxX() - 75 - 30 - uncertainyWidth;
		int maxY = (int) bound.getMaxY() - 70;

		float timeMinX = originX + (destinX - originX)
				/ (MBData.ONE_DAY_TIME - MBData.TRIP_STRAT_TIME)
				* (MBData.startTime - MBData.TRIP_STRAT_TIME);
		float timeMaxX = originX + (destinX - originX)
				/ (MBData.ONE_DAY_TIME - MBData.TRIP_STRAT_TIME)
				* (MBData.startTime + MBData.period - MBData.TRIP_STRAT_TIME);

		int minY = (int) (maxY - bound.getHeight() + 80);

		curTimeLeftLine = new Line2D.Float(timeMinX, minY, timeMinX, maxY);
		curTimeRightLine = new Line2D.Float(timeMaxX, minY, timeMaxX, maxY);

		g.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND,
				BasicStroke.JOIN_ROUND));
		g.drawLine((int) timeMinX, maxY, (int) timeMinX, minY);
		g.drawLine((int) timeMaxX, maxY, (int) timeMaxX, minY);
		g.drawLine((int) timeMinX, minY, (int) timeMaxX, minY);

		int[] polygonX = { (int) timeMinX, (int) timeMinX, (int) timeMaxX,
				(int) timeMaxX };
		int[] polygonY = { maxY, minY, minY, maxY };

		Polygon polygon = new Polygon(polygonX, polygonY, 4);
		g.setColor(new Color(125, 125, 125, 50));
		g.fillPolygon(polygon);
	}

	private void renderODList(Graphics2D g) {
		renderODList(g, data.getInitOrginSNList());
		renderODList(g, data.getInitDestinSNList());
	}

	public void renderODList(Graphics2D g, List<SankeyNodeGroup> snGroupList) {
		for (SankeyNodeGroup sng : snGroupList) {
			if (sng.getId().equals("BP"))
				continue;

			if (sng.hide)
				continue;

			renderSNGList(g, sng, sng);

			SankeyNodeGeometry geo = sng.getGeometry();

			if (geo == null)
				continue;

			geo.render(g);
		}
	}

	private void renderSNGList(Graphics2D g, SankeyNodeGroup rootSNG,
			SankeyNodeGroup sng) {
		if (sng.expanded()) {
			for (SankeyNodeGroup subSNG : sng.getNodeGroupList()) {
				if (sng.hide)
					continue;
				renderSNGList(g, rootSNG, subSNG);
			}
		} else {
			if (!sng.hide)
				renderSNG(g, rootSNG, sng);
		}
	}

	private void renderSNG(Graphics2D g, SankeyNodeGroup rootSNG,
			SankeyNodeGroup sng) {
		setColor(g, rootSNG);

		Rectangle bound = sng.getBound();
		if (sng.getBound() == null)
			return;

		g.fillRect((int) bound.getMinX(), (int) bound.getMinY(),
				(int) bound.getWidth(), (int) bound.getHeight());

		int idLength = g.getFontMetrics().stringWidth(sng.getId());

		float x = 0;
		if (sng.origin)
			x = bound.getMinX() - idLength - uncertainyWidth;
		else
			x = bound.getMaxX() + uncertainyWidth;
		float y = (bound.getMinY() + bound.getMaxY()) / 2;
		int height = g.getFontMetrics().getHeight() / 3;

		String id = sng.getId();
		if (rootSNG.getNodeList().size() == 1)
			id = rootSNG.getNodeList().get(0).getId();

		g.setColor(Color.BLACK);
		g.drawString(id, (int) x, (int) y + height);
	}

	private void setColor(Graphics2D g, SankeyNodeGroup sng) {
		if (sng.getId().equals("EW"))
			g.setColor(SGMRTColorTable.EW_COLOR);
		else if (sng.getId().equals("NS"))
			g.setColor(SGMRTColorTable.NS_COLOR);
		else if (sng.getId().equals("CC"))
			g.setColor(SGMRTColorTable.CC_COLOR);
		else if (sng.getId().equals("NE"))
			g.setColor(SGMRTColorTable.NE_COLOR);
		else if (sng.origin)
			g.setColor(Color.RED);
		else
			g.setColor(Color.BLUE);
	}

	private void renderODPair(Graphics2D g2d) {
		if (data.getODPairs() == null || data.getODPairs().size() == 0)
			return;

		float maxVolume = 0f;

		for (ODPair sp : data.getODPairs()) {
			SankeyNode originSN = sp.getOrigin();
			SankeyNode destinSN = sp.getDestin();

			if (originSN.getId().equals("Others")
					|| destinSN.getId().equals("Others"))
				continue;

			Map<SankeyNode, Float>[] allSankeyMap = originSN
					.getAllNodePairMap();
			for (int time = MBData.startTime; time < MBData.startTime
					+ MBData.period; time++) {
				Float volume = allSankeyMap[time - MBData.TRIP_STRAT_TIME]
						.get(destinSN);
				if (volume == null)
					volume = 0F;

				maxVolume = Math.max(maxVolume, volume);
			}
		}
		renderColorMap(g2d, 0, (int) maxVolume);
		renderODProbability(g2d);

		for (ODPair sp : data.getODPairs()) {
			SankeyNode originSN = sp.getOrigin();
			SankeyNode destinSN = sp.getDestin();

			Rectangle originSNBound = originSN.getNodePairBound(destinSN);
			if (originSNBound == null || originSNBound.getHeight() <= 3)
				continue;

			Rectangle destinSNBound = destinSN.getNodePairBound(originSN);

			if (originSN.getId().equals("Others")
					|| destinSN.getId().equals("Others")) {
				g2d.setColor(new Color(10, 10, 10, 100));

				g2d.fillRect((int) originSNBound.getMinX() + 1,
						(int) originSNBound.getMinY() + 1, 10,
						(int) originSNBound.getHeight() - 1);

				g2d.fillRect((int) destinSNBound.getMinX() - 11,
						(int) destinSNBound.getMinY() + 1, 10,
						(int) destinSNBound.getHeight() - 1);

				g2d.setColor(Color.BLACK);
				if (originSNBound.getHeight() > 15) {
					float startX = originSNBound.getMinX();
					float startY = originSNBound.getMinY()
							+ originSNBound.getHeight() / 2
							+ g2d.getFontMetrics().getHeight() / 3;

					g2d.drawString(sp.getDestin().getId(), startX, startY);

					float endX = destinSNBound.getMinX()
							- g2d.getFontMetrics().stringWidth(
									sp.getOrigin().getId()) - 3;
					float endY = destinSNBound.getMinY()
							+ destinSNBound.getHeight() / 2
							+ g2d.getFontMetrics().getHeight() / 3;

					g2d.drawString(sp.getOrigin().getId(), endX, endY);
				}

				continue;
			}

			List<Point2D> line1 = getSankeyLine(
					new Point2D(originSNBound.getMinX(),
							originSNBound.getMinY()),
					new Point2D(destinSNBound.getMinX(), destinSNBound
							.getMinY()));
			List<Point2D> line2 = getSankeyLine(
					new Point2D(originSNBound.getMinX(),
							originSNBound.getMaxY()),
					new Point2D(destinSNBound.getMinX(), destinSNBound
							.getMaxY()));

			int[] x = new int[line1.size() * 2];
			int[] y = new int[line1.size() * 2];
			for (int i = 0; i < x.length; i++) {
				if (i < line1.size()) {
					x[i] = MBTools.closestInteger(line1.get(i).getX());
					y[i] = MBTools.closestInteger(line1.get(i).getY());
				} else {
					x[i] = MBTools.closestInteger(line2.get(x.length - 1 - i)
							.getX());
					y[i] = MBTools.closestInteger(line2.get(x.length - 1 - i)
							.getY());
				}
			}

			g2d.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND,
					BasicStroke.JOIN_ROUND));
			Polygon p = new Polygon(x, y, x.length);
			g2d.setColor(new Color(125, 125, 125, 150));
			g2d.fillPolygon(p);

			g2d.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND,
					BasicStroke.JOIN_ROUND));
			g2d.setColor(Color.WHITE);
			g2d.drawPolygon(p);

			int iii = 144;
			Map<SankeyNode, Float>[] allSankeyMap = originSN
					.getAllNodePairMap();
			for (int time = MBData.startTime; time < MBData.startTime
					+ MBData.period; time++) {
				int timeDiff = time - MBData.startTime;

				int[] sliceX = new int[(iii * TIME_INTERVAL_SIZE
						/ MBData.period + 1) * 2];
				int[] sliceY = new int[(iii * TIME_INTERVAL_SIZE
						/ MBData.period + 1) * 2];
				for (int k = 0; k < sliceX.length; k++) {
					int lineIndex = timeDiff * iii * TIME_INTERVAL_SIZE
							/ MBData.period + k;
					if (k < iii * TIME_INTERVAL_SIZE / MBData.period + 1) {
						sliceX[k] = MBTools.closestInteger(line1.get(lineIndex)
								.getX());
						sliceY[k] = MBTools.closestInteger(line1.get(lineIndex)
								.getY());
					} else {
						lineIndex = timeDiff
								* iii
								* TIME_INTERVAL_SIZE
								/ MBData.period
								+ (iii * TIME_INTERVAL_SIZE / MBData.period + 1)
								* 2 - 1 - k;
						sliceX[k] = MBTools.closestInteger(line2.get(lineIndex)
								.getX());
						sliceY[k] = MBTools.closestInteger(line2.get(lineIndex)
								.getY());
					}
				}

				Float volume = allSankeyMap[time - MBData.TRIP_STRAT_TIME]
						.get(destinSN);
				if (volume == null)
					volume = 1F;

				p = new Polygon(sliceX, sliceY, sliceX.length);
				int colorY = (int) ((imageHeight - 1) * Math.log(volume) / Math
						.log(maxVolume));
				colorY = Math.min(colorY, imageHeight - 1);
				int rGB = colorMap.getRGB(0, colorY);

				int b = rGB & 0xff; // blue
				int g = ((rGB >>> 8)) & 0xff; // green
				int r = (rGB >>> 16) & 0xff; // red

				g2d.setColor(new Color(r, g, b, 150));

				g2d.fillPolygon(p);
			}

			g2d.setColor(Color.BLACK);
			if (originSNBound.getHeight() > 15) {
				float startX = originSNBound.getMinX();
				float startY = originSNBound.getMinY()
						+ originSNBound.getHeight() / 2
						+ g2d.getFontMetrics().getHeight() / 3;

				g2d.drawString(sp.getDestin().getId(), startX, startY);

				float endX = destinSNBound.getMinX()
						- g2d.getFontMetrics().stringWidth(
								sp.getOrigin().getId()) - 3;
				float endY = destinSNBound.getMinY()
						+ destinSNBound.getHeight() / 2
						+ g2d.getFontMetrics().getHeight() / 3;

				g2d.drawString(sp.getOrigin().getId(), endX, endY);
			}
		}
	}

	private void renderODProbability(Graphics2D g2d) {
		for (ODPair sp : data.getODPairs()) {
			SankeyNode originSN = sp.getOrigin();

			SankeyNode destinSN = sp.getDestin();

			// if(originSN.getId().equals("EW26") &&
			// destinSN.getId().equals("EW15"))
			// System.out.println(originSN.getId() + "->" + destinSN.getId()
			// + " prob " + sp.getProbability());

			Rectangle originSNBound = originSN.getNodePairBound(destinSN);
			if (originSNBound == null || originSNBound.getHeight() == 0)
				continue;

			Rectangle destinSNBound = destinSN.getNodePairBound(originSN);

			// render uncertainty
			int uncertainty = 50 + (int) (100 * sp.getProbability());

			// if (originSN.getId().equals("Others")
			// || destinSN.getId().equals("Others"))
			// uncertainty = 100;

			g2d.setColor(new Color(10, 10, 10, uncertainty));
			g2d.fillRect((int) originSNBound.getMinX() - 30 - uncertainyWidth
					- 1, (int) originSNBound.getMinY() + 1, uncertainyWidth,
					(int) originSNBound.getHeight() - 1);
			g2d.fillRect((int) destinSNBound.getMinX() + 30 + 1,
					(int) destinSNBound.getMinY() + 1, uncertainyWidth,
					(int) destinSNBound.getHeight() - 1);
		}
	}

	private List<Point2D> getSankeyLine(Point2D p1, Point2D p2) {
		int plSize = 144 * TIME_INTERVAL_SIZE + 1;
		List<Point2D> pl = new ArrayList<Point2D>();

		float delta = (p2.getX() - p1.getX()) / (plSize - 1);
		float theta = (p2.getY() - p1.getY()) / (p2.getX() - p1.getX());

		float a = 2 * theta / (p2.getX() - p1.getX());
		float b = -2 * a * p1.getX();
		float c = p1.getY() - a * p1.getX() * p1.getX() - b * p1.getX();

		for (int i = 0; i < plSize / 2; i++) {
			float x = p1.getX() + delta * i;
			float y = a * x * x + b * x + c;

			pl.add(new Point2D(x, y));
		}

		a = 2 * theta / (p1.getX() - p2.getX());
		b = -2 * a * p2.getX();
		c = p2.getY() - a * p2.getX() * p2.getX() - b * p2.getX();

		for (int i = plSize / 2; i < plSize; i++) {
			float x = p1.getX() + delta * i;
			float y = a * x * x + b * x + c;
			pl.add(new Point2D(x, y));
		}

		return pl;
	}

	public void calODPositions() {
		float width = 30;
		float originHeight = bound.getHeight()
				- 75
				- (data.getInitOrginSNList().size() - hiddenSNNum(data
						.getInitOrginSNList())) * inter_group_gap;
		float destinHeight = bound.getHeight()
				- 75
				- (data.getInitDestinSNList().size() - hiddenSNNum(data
						.getInitDestinSNList())) * inter_group_gap;

		originX = bound.getMinX() + 65 + uncertainyWidth;
		destinX = bound.getMaxX() - 75 - width - uncertainyWidth;

		float y = bound.getMaxY() - 75;

		calODPositions(originX, y, width, originHeight,
				data.getInitOrginSNList(), true);
		calODPositions(destinX, y, width, destinHeight,
				data.getInitDestinSNList(), false);

		calODPairPosition(data.getInitOrginSNList());
		calODPairPosition(data.getInitDestinSNList());

		originX += width;
	}

	private int hiddenSNNum(List<SankeyNodeGroup> sngList) {
		int hiddenNum = 0;
		for (SankeyNodeGroup sng : sngList)
			if (sng.hide)
				hiddenNum++;
		return hiddenNum;
	}

	private void calODPairPosition(List<SankeyNodeGroup> sngList) {
		for (int i = 0; i < sngList.size(); i++) {
			SankeyNodeGroup sng = sngList.get(i);

			calODPairPosition(sng);
		}
	}

	private void calODPairPosition(SankeyNodeGroup sng) {
		if (sng.expanded()) {
			for (SankeyNodeGroup subnode : sng.getNodeGroupList())
				calODPairPosition(subnode);
		} else
			sng.calODPairBound();
	}

	private void calODPositions(float x, float y, float width, float height,
			List<SankeyNodeGroup> nodeList, boolean origin) {
		SankeyNode.sortByVolume(nodeList);

		int totalVolume = 0;
		for (SankeyNodeGroup sng : nodeList)
			totalVolume += sng.getVolume();

		for (int i = 0; i < nodeList.size(); i++) {
			SankeyNodeGroup sng = nodeList.get(i);
			if (sng.hide)
				continue;

			float nodeHeight = sng.getVolume() * 1.0f / totalVolume * height;

			Rectangle nodeBound = new Rectangle(x, y - nodeHeight, x + width, y);
			sng.setBound(nodeBound);

			Rectangle geometryBound = null;
			if (origin)
				geometryBound = new Rectangle(bound.getMinX() - geoBoundSize, y
						- nodeHeight / 2 - geoBoundSize / 2, bound.getMinX(), y
						- nodeHeight / 2 + geoBoundSize / 2);
			else
				geometryBound = new Rectangle(bound.getMaxX(), y - nodeHeight
						/ 2 - geoBoundSize / 2, bound.getMaxX() + geoBoundSize,
						y - nodeHeight / 2 + geoBoundSize / 2);
			sng.setGeometry(data, geometryBound);

			if (sng.expanded()) {
				sng.calPosition();
			}
			y = y - nodeBound.getHeight() - inter_group_gap;
		}

		int geoBoundY = 30 + inter_group_gap;
		for (int i = nodeList.size() - 1; i >= 0; i--) {
			SankeyNodeGroup node = nodeList.get(i);
			Rectangle geoBound = node.getGeometry().getBound();

			if (node.getId().equals("BP") || node.hide)
				continue;

			if (geoBound.getMinY() < geoBoundY) {
				Rectangle newGeo = new Rectangle(geoBound.getMinX(), geoBoundY,
						geoBound.getMaxX(), geoBoundY + geoBoundSize);
				node.setGeometry(data, newGeo);
				geoBoundY += (geoBoundSize + inter_group_gap);
			}
		}
	}

	public Line2D.Float getCurTimeLeftLine() {
		return curTimeLeftLine;
	}

	public Line2D.Float getCurTimeRightLine() {
		return curTimeRightLine;
	}

	public ODSankeyDiagramData getData() {
		return data;
	}

	public Rectangle getBound() {
		return bound;
	}
}
